#coding=utf-8
"""
LoNote intermediation 中介格式模組

定義 LoNote 標準資料格式，並提供操作的 API。
負責將其他資料格式，轉換為 LoNote 的標準資料格式。

=================================

參數命名法表記如下：

    fpath    = 筆記簿存取路徑
    uuid     = 純文字格式的 uuid。由 str(uuid.UUID) 而來，格式類似於：
               adb87d45-c5de-4041-9f7a-6667a19d93c3
               在某些函式中可為 None，藉此表示筆記簿本身。
    title    = 供人類閱讀的筆記標題，字串。可重名。
    content  = html utf-8 格式的純文字內容
    data     = bytes 格式的資料
    order    = 父筆記底下的順序。
               從 0 起算，None 表示尾部。
    name     = 任何可以作為識別字的字串，表示同一個 uuid 下資源或歷史的名稱
               資源與歷史的名稱各自使用不同的命名空間。
    comment  = 歷史記錄被寫入時，必須要附上的註解
    ftime    = frozen time，record 的凍結時間
    res_type = 資源類型，字串。
               預定有被系統使用的值為 'emb' 與 'att'。
               但事實上傳入其他值也能被正常處理。
    key      = extra 用的索引。在整個 im 中有唯一的指向對象。
               extra 為全筆記簿共用的某種未定義資料，
               可能為任何可被 pickle 的物件。

=================================

前後綴表記法如下：

    loc 前綴    = "load or create" 的縮寫。
                  表示嘗試讀取，如果不存在則新建它。
    e 前綴      = 為 xml.eTree.ElementTree 的 element 物件
    後綴 create = 表示新建一筆資料。和已有資料衝突會拋出 IMAreadyExist
    後綴 update = 表示更新一筆資料，如果資料不存在會試圖自動新建。
                  操作失敗時會拋出 IMError 底下的各種錯誤
                  有少數 update 項目無法自動新建，得先經過 create。
    後綴 delete = 表示刪除一筆資料，資料不存在時會拋出 IMNotExist

"""

import os
import os.path
import xml.etree.ElementTree as ET
import datetime as DT
import functools
import pickle
import uuid as UM # Uuid Module 命名碰撞太多所以改個名

import lonote.civa.slots
from lonote.civa.tz import local_timezone

from .tar import Tar, v_normpath

class IMError(Exception):
    '''基本錯誤類別，不該直接使用'''
    pass
class IMValueError(IMError):
    '''輸入參數值無法被正確處理時，跳出本錯誤'''
    pass
class IMNotExist(IMError):
    '''操作時發現有重要的東西不存在時，跳出本錯誤'''
    pass
class IMAlreadyExist(IMError):
    '''操作時發現有東西佔住位置擋路時，跳出本錯誤'''
    pass

class _vpath:
    '''底層 Tar 物件中的虛擬路徑'''
    bookmeta = 'bookmeta.xml'
    extrakeys = 'extrakeys'
    def content(uuid):
        '''筆記 content 的虛擬路徑'''
        content_path = os.path.join(uuid, 'content.html')
        content_path_norm = v_normpath(content_path)
        return content_path_norm
    def resource(uuid, name):
        '''筆記 resource 的虛擬路徑'''
        res_dir = _vpath.__resource_dir(uuid)
        res_path = os.path.join(res_dir, name)
        res_path_norm = v_normpath(res_path)
        return res_path_norm
    def history(uuid):
        '''筆記的 history 路徑'''
        histpath = os.path.join(uuid, 'history.xml')
        histpath_norm = v_normpath(histpath)
        return histpath_norm
    def resource_xml(uuid):
        '''筆記的 resource.xml 路徑'''
        resxml = os.path.join(uuid, 'resource.xml')
        resxml_norm = v_normpath(resxml)
        return resxml_norm
    def record(uuid, filename):
        '''筆記的特定一筆 history 檔案路徑'''
        histpath = os.path.join(_vpath.__history_dir(uuid), filename)
        histpath_norm = v_normpath(histpath)
        return histpath_norm
    def extra(key):
        '''extra資料的存取路徑'''
        extrapath = os.path.join('extra', key)
        extrapath_norm = v_normpath(extrapath)
        return extrapath_norm

    def __history_dir(uuid):
        return os.path.join(uuid, 'history')
    def __resource_dir(uuid):
        return os.path.join(uuid, 'resource')

def _gen_enote(uuid, title):
    '''產生一個 uuid = uuid 的 enote'''
    dt = DT.datetime.now(local_timezone()).strftime(IM.time_format)

    e = ET.Element('note', uuid = str(uuid))

    ET.SubElement(e, 'title').text = str(title)
    ET.SubElement(e, 'created_time').text = dt
    ET.SubElement(e, 'modified_time').text = dt
    ET.SubElement(e, 'filename').text = _vpath.content(uuid)
    return e

def _gen_ehistory(uuid):
    '''產生一個 history element'''
    e = ET.Element('history', uuid = uuid)
    return e

def _gen_erecord(uuid, name, comment, ftime):
    '''產生一筆 history 的 record element'''
    ftime_text = ftime.strftime(IM.time_format)

    e = ET.Element('record', name = name)

    ET.SubElement(e, 'frozen_time').text = ftime_text
    ET.SubElement(e, 'comment').text = comment
    ET.SubElement(e, 'filename').text = _vpath.record(uuid,
            name + '.html')
    return e

def _gen_eresource_xml(uuid):
    '''產生一個 resource_xml element'''
    e = ET.Element('resource_xml', uuid = uuid)
    return e

def _gen_eresource(uuid, name, res_type):
    '''產生一筆 recource 的 element'''
    e = ET.Element('resource', name = name, res_type = res_type)
    ET.SubElement(e, 'filename').text = _vpath.resource(uuid, name)
    return e

def _filename(e):
    '''取出 element 中的 filename

        如果沒有 filename 則拋出 IMNotExist
        '''
    filename = e.findtext('./filename')
    if filename is None: # 沒有找到
        raise IMNotExist('This element without "filename"!')
    else:
        return filename

class IM:
    '''中介資料物件

        可以透過語義，對筆記簿的各部件進行讀寫操作
        '''

    time_format = '%Y-%m-%dT%H:%M:%S.%f%z'
    new_note_content = ''
    unnamed_note_name = 'Unnamed Note'
    ext = '.lobook'

    # 類別函式

    def is_this_type(fpath):
        '''透過 fpath 探測格式。格式正確回傳 True 否則回傳 False'''
        before, ext = os.path.splitext(fpath)
        if ext == IM.ext:
            return True
        else:
            return False

    def transfer(from_im, to_im, uuid, mapper = dict()):
        '''將from_im的uuid資料複製到to_im的note中(目標note需預先存在)

            mapper = 為一 from_im_uuid -> to_im_uuid 字典
                     如果沒有適當的映射值，則使用相同的uuid
                     另外 mapper 也會被用做 content 內容替換之用
                     (以維持內部連結儘量保持連上而不因此斷開)
            '''
        to_uuid = mapper.get(uuid)
        if to_uuid is None:
            to_uuid = uuid

        # 讀出資料
        title = from_im.title(uuid)
        content = from_im.content(uuid)
        for for_from_uuid, for_to_uuid in mapper.items(): # 替換content內部連結
            content = content.replace(for_from_uuid, for_to_uuid)
        mtime = from_im.mtime(uuid)
        ctime = from_im.ctime(uuid)
        resource_list = from_im.resource_list(uuid)
        record_list = from_im.record_list(uuid)
        # 寫入資料
        to_im.title_update(to_uuid, title = title)
        to_im.content_update(to_uuid, content = content)
        to_im.note_time_update(to_uuid, mtime = mtime, ctime = ctime)
        for res_info in resource_list:
            to_im.resource_update(to_uuid,
                    name = res_info['name'],
                    res_type = res_info['res_type'],
                    data = from_im.resource(uuid, res_info['name']))
        for rec_info in record_list:
            to_im.record_create(to_uuid,
                    name = rec_info['name'],
                    comment = rec_info['comment'],
                    content = from_im.record(uuid, name = rec_info['name']),
                    ftime = rec_info['ftime'])

    # 初始化

    def __init__(self, fpath, bookuuid = None):
        '''輸入資料位置，嘗試讀取資料檔案

            如果載入過程中發現檔案不完整則立刻初始化

            fpath  = tar 資料檔位置
            bookuuid = 如果有指定，使用指定的bookuuid生成新book
                       讀取時如果bookuuid有指定且不符合，則拋出IMNotExist
            return ==> (tar, ebook)
            '''
        # 確認路徑合法
        if IM.is_this_type(fpath) == False:
            info = 'Filename ext must "{}"!'.format(IM.ext)
            raise IMValueError(info)

        self.fpath = fpath # 記錄 fpath
        self.__tar = Tar(fpath) # 載入或初始化tar檔案
        try:
            data = self.__tar.load(_vpath.bookmeta, mode = 'txt')
            self.__ebook = ET.fromstring(data)
            if bookuuid and bookuuid != self.book_uuid():
                info = 'You assign the bookuuid, but not match this book!'
                raise IMNotExist(info)
        except KeyError: # 嘗試讀取tar中資料，如果沒有則表示尚未初始化
            # 建立 root note
            if not bookuuid:
                bookuuid = str(UM.uuid4())
            self.__ebook = _gen_enote(bookuuid, 'Unnamed Notebook')
            xml = ET.tostring(self.__ebook, encoding = 'unicode')
            self.__tar.update(_vpath.bookmeta, data = xml, mode = 'txt')
            self.content_update(bookuuid, IM.new_note_content)

    # 內部函式

    def __is_root(self, uuid):
        '''檢查uuid是不是代表root

            uuid = 空字串、None、root 的 uuid 時回傳 True
                   其他時刻回傳False
            '''
        if uuid is None or uuid == '' or self.__ebook.get('uuid') == uuid:
            return True
        else:
            return False

    def __e_note(self, uuid):
        '''取得指定的 element

            uuid = 要取回的 enote 的 uuid
                   None 表示取回筆記簿本身，就是 self.__ebook
            '''
        # 檢測是不是指 book
        if self.__is_root(uuid):
            return self.__ebook

        # 不是指 book，用一般的方式搜尋
        match = './/note[@uuid="{}"]'.format(uuid)
        e = self.__ebook.find(match)
        if e is None:
            info = 'Note not exist: uuid = "{}"'.format(uuid)
            raise IMNotExist(info)
        else:
            return e

    def __e_parent(self, uuid):
        '''取得指定 uuid 的 parent

            注意： parent 不存在時會拋出 IMNotExist
        '''
        if self.__is_root(uuid):
            info = 'Root page didn\'t have parent!'
            raise IMNotExist(info)

        match = './/note[@uuid="{}"]..'.format(uuid)
        e = self.__ebook.find(match)
        if e is None:
            info = 'Parent note not exist: uuid = "{}"'.format(uuid)
            raise IMNotExist(info)
        else:
            return e

    def __e_resource_xml(self, uuid):
        '''回傳資源xml的element'''
        resxml = self.resource_xml(uuid)
        e_resource_xml = ET.fromstring(resxml)
        return e_resource_xml

    def __e_resource_xml_save(self, uuid, e_resource_xml):
        '''寫入指定筆記的 resource xml

            如果沒有子元素，則直接將xml檔案刪除
            '''
        resxml_path = _vpath.resource_xml(uuid)
        if len(e_resource_xml) == 0: # 沒有子元素則刪除
            self.__tar.delete(resxml_path)
        else: # 更新
            history_xml = ET.tostring(e_resource_xml, encoding = 'unicode')
            self.__tar.update(resxml_path, data = history_xml, mode = 'txt')
        # 清除緩存
        self.resource_xml.cache_clear()
        self.resource_list.cache_clear()
        self.resource.cache_clear()

    def __e_resource(self, uuid, name, res_type = None):
        '''回傳資源的 element

            如果 res_type 有給值，則必須要與找到的目標類型符合，否則會跳出
            IMValueError。如果沒有給值，則忽略 res_type。
            除此之外如果找不到指定資源，會跳出 IMNotExist 錯誤。
            '''
        e_resxml = self.__e_resource_xml(uuid)
        match = './resource[@name="{}"]'.format(name)
        e_res = e_resxml.find(match)
        if e_res is not None:
            if res_type is not None and e_res.get('res_type') != res_type:
                info = 'Resource "{}" type not match!'.format(name)
                raise IMValueError(info)
            return e_res
        else:
            info = 'Resource "{}" not Exist!'.format(name)
            raise IMNotExist(info)

    def __children_title(self, enote):
        '''產生一個含有所有 children title 的集合

            enote = 當前筆記的 element
            '''
        return {etitle.text for etitle in enote.iterfind('./note/title')}

    def __e_history(self, uuid):
        '''讀出指定筆記的 histroy element'''
        histxml = self.history_xml(uuid)
        e_history = ET.fromstring(histxml)
        return e_history

    def __e_record(self, uuid, name):
        '''讀出指定筆記的其中一個記錄項'''
        e_history = self.__e_history(uuid)
        match = './record[@name="{}"]'.format(name)
        e_record = e_history.find(match)
        if e_record is None:
            info = 'Record "{}" in note uuid == "{}" are not exist!'.format(
                    name, uuid)
            raise IMNotExist(info)
        return e_record

    def __e_history_save(self, uuid, e_history):
        '''寫入指定筆記的 histroy xml

            如果沒有子元素，則直接將xml檔案刪除
            '''
        histpath = _vpath.history(uuid)
        if len(e_history) == 0: # 沒有子元素則刪除
            self.__tar.delete(histpath)
        else: # 更新
            history_xml = ET.tostring(e_history, encoding = 'unicode')
            self.__tar.update(histpath, data = history_xml, mode = 'txt')
        self.history_xml.cache_clear()

    def __bookmeta_save(self):
        '''儲存當前的 bookmeta 到內部檔案中'''
        self.__tar.update(
                _vpath.bookmeta, data = self.book_xml(), mode = 'txt')

        # 清除緩存
        self.book_xml.cache_clear()
        self.position.cache_clear()

    # 雜項功能

    def flush(self, mode = 'w'):
        '''將資料壓入'''
        self.__tar.flush(mode)

    def flushed(self):
        '''檢查是否已 flush'''
        self.__tar.flushed()

    def im_export(self, uuid, new_fpath):
        '''匯出某節點為一本單獨的筆記簿'''
        def recv(uuid):
            '''將uuid及其下方所有筆記都轉移到newim中'''
            IM.transfer(from_im = self, to_im = newim, uuid = uuid)
            for child_uuid in self.children_list(uuid):
                newim.note_create(
                        parent_uuid = uuid, new_uuid = child_uuid)
                recv(child_uuid)

        if not new_fpath.endswith(IM.ext):
            new_fpath = new_fpath + IM.ext
        # 確定目標檔案不存在
        if os.path.exists(new_fpath):
            info = '"{}" already exist!'.format(new_fpath)
            raise IMAlreadyExist(info)

        # 把筆記資料複製過去
        newim = IM(new_fpath, bookuuid = uuid)
        recv(uuid)
        # 複製 extra
        for key in self.extra_keys():
            newim.extra_update(key, obj = self.extra(key))
        # flush!
        newim.flush()

    def im_import(self, parent_uuid, in_fpath):
        '''匯入一個完整的筆記本到當前 uuid 筆記頁的下方

            回傳有更動的舊 uuid -> 新 uuid 的字典
            '''
        def get_conflict_replacer(inim):
            '''取得衝突uuid的替代uuid dict'''
            # 計算inim內部有無uuid與self內重複
            in_ebook = ET.fromstring(inim.book_xml())
            in_uuids = {e.get('uuid') for e in in_ebook.iter('note')}
            ebook = self.__e_note(None)
            uuids = {e.get('uuid') for e in ebook.iter('note')}
            intersection_uuids = in_uuids & uuids
            # 產生新uuid映射表
            def gen_replacer():
                uuid_map = {iuuid: str(UM.uuid4()) for iuuid in intersection_uuids}
                new_uuids = {new_uuid for new_uuid in uuid_map.values()}
                if len((uuids | in_uuids) & new_uuids) == 0:
                    return uuid_map
                else:
                    return gen_replacer()
            return gen_replacer()

        def mapto(uuid, mapper):
            ans = mapper.get(uuid)
            if ans is None:
                return uuid
            else:
                return ans

        def recv(parent_uuid, transfer_uuid):
            '''將uuid及其下方所有筆記都轉移到newim中'''
            new_transfer_uuid = mapto(transfer_uuid, uuid_map)
            self.note_create(parent_uuid = parent_uuid,
                    new_uuid = new_transfer_uuid)

            IM.transfer(from_im = inim,
                    to_im = self, uuid = transfer_uuid, mapper = uuid_map)
            for child_uuid in inim.children_list(transfer_uuid):
                recv(new_transfer_uuid, child_uuid)

        enote = self.__e_note(parent_uuid) # 不存在則拋出錯誤
        parent_uuid = enote.get('uuid')

        # 初始化
        if not in_fpath.endswith(IM.ext):
            in_fpath = in_fpath + IM.ext
        # 確定目標檔案存在
        if not os.path.exists(in_fpath):
            info = '"{}" not exist!'.format(in_fpath)
            raise IMNotExist(info)

        inim = IM(in_fpath)
        uuid_map = get_conflict_replacer(inim)
        in_root_uuid = inim.book_uuid()

        # 套入
        recv(parent_uuid, in_root_uuid)

        return uuid_map

    # 讀取功能

    def book_uuid(self):
        '''回傳 book 本身的 uuid'''
        return self.__ebook.get('uuid')

    @functools.lru_cache()
    def book_xml(self):
        '''回傳當前 ebook 的 xml 純文字'''
        return ET.tostring(self.__ebook, encoding = 'unicode')

    @functools.lru_cache()
    def history_xml(self, uuid):
        '''讀出指定筆記的 histroy xml'''
        histpath = _vpath.history(uuid)
        try:
            return self.__tar.load(histpath, mode = 'txt')
        except KeyError: # 如果讀不到這個檔案則使用初始值
            ehistory = _gen_ehistory(uuid)
            xml = ET.tostring(ehistory, encoding = 'unicode')
            return xml

    @functools.lru_cache(None)
    def resource_xml(self, uuid):
        '''讀出指定筆記的 resource xml'''
        respath = _vpath.resource_xml(uuid)
        try:
            return self.__tar.load(respath, mode = 'txt')
        except KeyError: # 如果讀不到這個檔案則使用初始值
            eresource_xml = _gen_eresource_xml(uuid)
            xml = ET.tostring(eresource_xml, encoding = 'unicode')
            return xml

    def title(self, uuid):
        '''回傳指定 uuid 的 title'''
        return self.__e_note(uuid).findtext('./title')

    def mtime(self, uuid):
        '''獲取指定筆記的更新時間'''
        enote = self.__e_note(uuid)
        time_text = enote.findtext('./modified_time')
        dt = DT.datetime.strptime(time_text, IM.time_format)
        return dt

    def ctime(self, uuid):
        '''獲取指定筆記的建立時間'''
        enote = self.__e_note(uuid)
        time_text = enote.findtext('./created_time')
        dt = DT.datetime.strptime(time_text, IM.time_format)
        return dt

    def ftime(self, uuid, name):
        '''獲取指定歷史記錄的凍結時間'''
        e_rec = self.__e_record(uuid, name)
        time_text = e_rec.findtext('./frozen_time')
        dt = DT.datetime.strptime(time_text, IM.time_format)
        return dt

    @functools.lru_cache()
    def position(self, uuid):
        '''回傳 (parent_uuid, order)

            order 從0起算，且只計算note型節點
            '''
        eparent = self.__e_parent(uuid)
        enote = self.__e_note(uuid)
        subenotes = eparent.findall('./note')
        for i in range(len(subenotes)):
            if enote == subenotes[i]:
                return (eparent.get('uuid'), i)

    def uuid_before(self, uuid):
        '''取得同級的前一個 uuid，如不存在則拋出IMNotExist例外'''
        eparent = self.__e_parent(uuid)
        enote = self.__e_note(uuid)
        subenotes = eparent.findall('./note')
        for i in range(len(subenotes)):
            if enote == subenotes[i]:
                if i >= 1:
                    return subenotes[i - 1].get('uuid')
                else:
                    raise IMNotExist('This is the first note.')

    def uuid_next(self, uuid):
        '''取得同級的後一個 uuid，不存在則拋出IMNotExist例外'''
        eparent = self.__e_parent(uuid)
        enote = self.__e_note(uuid)
        subenotes = eparent.findall('./note')
        for i in range(len(subenotes)):
            if enote == subenotes[i]:
                try:
                    target_enote = subenotes[i + 1]
                except IndexError:
                    raise IMNotExist('This is the last note.')
                return target_enote.get('uuid')

    def uuid_first(self, uuid):
        '''取得同級的第一個 uuid'''
        eparent = self.__e_parent(uuid)
        first_enote = eparent.find('./note')
        return first_enote.get('uuid')

    def uuid_last(self, uuid):
        '''取得同級的最後一個 uuid'''
        eparent = self.__e_parent(uuid)
        subenotes = eparent.findall('./note')
        last_enote = subenotes[-1]
        return last_enote.get('uuid')

    @functools.lru_cache()
    def content(self, uuid):
        '''回傳指定 uuid 的正文內容'''
        enote = self.__e_note(uuid)
        content_path = _filename(enote)
        ans = self.__tar.load(content_path, mode = 'txt')

        # print('bookmeta:', self.book_xml.cache_info(),
        #         '\ncontent:', self.content.cache_info(),
        #         '\nresource:', self.resource.cache_info()
        #         )
        return ans

    @functools.lru_cache()
    def resource(self, uuid, name, res_type = None):
        '''回傳指定筆記的附屬資源

            uuid     = 指定筆記的 uuid
            name     = 資源名稱，不含路徑
            res_type = 'emb' 為 內嵌元件
                       'att' 為 附加元件
            '''
        e_res = self.__e_resource(uuid, name, res_type)
        # 讀入檔案
        filename = _filename(e_res)
        return self.__tar.load(filename, mode = 'bytes')

    @functools.lru_cache(None)
    def resource_list(self, uuid):
        '''列出uuid下的所有resource'''
        all_res = self.__e_resource_xml(uuid).findall('./resource')
        data = [{
            'name': e_res.get('name'),
            'res_type': e_res.get('res_type'),
            # 'filename': e_res.findtext('./filename')
            }
            for e_res in all_res]
        return data

    def record(self, uuid, name):
        '''讀出指定 history record 的內容

            uuid = 筆記的 uuid
            name = record 的名稱
            '''
        e_record = self.__e_record(uuid, name)
        record_file = _filename(e_record)
        content = self.__tar.load(record_file, mode = 'txt')
        return content

    def record_list(self, uuid):
        '''列出uuid下的所有record'''
        ehist = self.__e_history(uuid)
        erec_list = ehist.findall('./record')
        data = [{
            'name': erec.get('name'),
            'ftime': self.ftime(uuid, erec.get('name')),
            'comment': erec.findtext('./comment'),
            # 'filename': erec.findtext('./filename')
            }
            for erec in erec_list]
        return data

    def children_list(self, uuid):
        '''依序列出uuid下所有的children之uuid'''
        enote = self.__e_note(uuid)
        return [e.get('uuid') for e in enote.findall('./note')]

    @functools.lru_cache()
    def extra(self, key):
        '''讀出指定的extra物件

            如果指定物件不存在，回傳 None
            '''
        extrapath = _vpath.extra(key)
        try:
            data = self.__tar.load(extrapath)
        except KeyError:
            return None
        obj = pickle.loads(data)
        return obj

    @functools.lru_cache(None)
    def extra_keys(self):
        '''回傳所有現存的extra的keys set'''
        try:
            data = self.__tar.load(_vpath.extrakeys)
            extrakeys = pickle.loads(data)
            return extrakeys
        except KeyError:
            return set()

    # 更新功能

    def note_time_update(self, uuid, mtime = None, ctime = None):
        '''強制更新筆記的時間

            uuid  = 企圖修改的筆記的 uuid
            mtime, ctime = 修改時間、創建時間。為 datetime 格式。
                           None 表示不修改
            '''
        enote = self.__e_note(uuid)
        if mtime is not None:
            if not mtime.tzname():
                mtime = mtime.replace(tzinfo = local_timezone())
            mtime_text = mtime.strftime(IM.time_format)
            enote.find('./modified_time').text = mtime_text
        if ctime is not None:
            if not ctime.tzname():
                ctime = ctime.replace(tzinfo = local_timezone())
            ctime_text = ctime.strftime(IM.time_format)
            enote.find('./created_time').text = ctime_text
        self.__bookmeta_save()

    def title_update(self, uuid, title = None):
        '''變更指定筆記的 title

            uuid  = 筆記的 uuid
            title = 新的筆記名
                    None 表示自動產生，但root時不能自動產生
            '''
        enote = self.__e_note(uuid)
        etitle = enote.find('./title')
        if title is not None:
            etitle.text = title
        else: # 如果 title 需自動產生，則……
            if self.__is_root(uuid):
                raise IMValueError('Can not auto generate the title!')
            eparent = self.__e_parent(uuid)
            occupied_titles = self.__children_title(eparent)
            restrict = {occupied_title for occupied_title
                    in occupied_titles
                    if occupied_title != etitle.text} # 把自己本身濾掉
            etitle.text = lonote.civa.slots.slotof(
                    IM.unnamed_note_name,
                    resrtict = restrict)
        self.__bookmeta_save()

    def content_update(self, uuid, content):
        '''更新 uuid 所存放的 content'''
        enote = self.__e_note(uuid)
        content_path = _filename(enote)
        self.__tar.update(content_path, data = content, mode = 'txt')
        # 更新時間
        self.note_time_update(uuid, mtime = DT.datetime.now())
        # 回存
        self.__bookmeta_save()
        # 清除緩存
        self.content.cache_clear()

    def position_update(self, uuid, parent_uuid = None, order = None):
        '''更新某個筆記在 notebook 中的結構位置

            uuid        = 要變更位置的筆記之 uuid
            parent_uuid = 新位置的父筆記頁之 uuid
                          None 表示筆記本直屬的最高層級（沒有父筆記頁）
            order       = 新位置下的順序, 從 0 開始計算
                          None 表示接續在最後
            '''
        def get_insert_order(order):
            '''轉換實際插入順序

                因為note節點之間混了resource節點與title等metadata節點
                所以實際插入的順序值和指定的順序值並不完全一樣
                要轉譯
                '''
            new_parent = self.__e_parent(uuid)
            allsub = new_parent.findall('./note')
            if len(allsub) <= order: # 如果order過大，實際插入點設在最下方
                return len(new_parent)
            elif order < 0:
                return 0
            else:
                # 依據order，到時要把下面這個note往後擠
                replace_note = allsub[order]

            insert_order = 0 # 計算實體插入點
            for i in range(len(new_parent)):
                if replace_note == new_parent[i]:
                    insert_order = i
                    break
            return insert_order

        if self.__is_root(uuid):
            raise IMValueError('Root note can not change position!')

        enote = self.__e_note(uuid)
        new_parent = self.__e_note(parent_uuid)
        # 如果要移動note移動到自己的子結點下面，則會成為環形迴路，
        # 這種事不可被允許，因此要檢查……
        match = './/note[@uuid="{}"]'.format(new_parent.get('uuid'))
        if enote.find(match) is not None:
            info = 'Can\'t move under child note.'
            raise IMValueError(info)
        elif uuid == new_parent.get('uuid'):
            info = 'Can\'t move under it self!'
            raise IMValueError(info)

        if order is None:
            old_parent = self.__e_parent(uuid)
            old_parent.remove(enote)
            new_parent.append(enote)
        else:
            ins_order = get_insert_order(order)
            old_parent = self.__e_parent(uuid)
            old_parent.remove(enote)
            new_parent.insert(ins_order, enote)
        self.__bookmeta_save()

    def resource_update(self, uuid, name, res_type, data):
        '''更新指定筆記的附屬資源

            如果 name 已存在且 res_type 相同，會更新資料
            如果 name 已存在且 res_type 不同，會拋出IMAlreadyExist

            uuid     = 指定筆記的 uuid
            name     = 資源名稱，不含路徑
                       emb與att使用相同的命名空間，不能衝突
            res_type = 'emb' 為 內嵌元件
                       'att' 為 附加元件
            data     = byte string 格式的資料
            '''
        # 先寫入 bookmeta
        try:
            e_res = self.__e_resource(uuid, name, res_type)
        except IMNotExist: # 如果指定資源原本不存在，新增之
            e_resxml = self.__e_resource_xml(uuid)
            e_res = _gen_eresource(uuid, name, res_type)
            e_resxml.append(e_res)
            self.__e_resource_xml_save(uuid, e_resxml)
        except IMValueError: # 如果指定資源存在但模式衝突
            info = 'Resource "{}" already Exist but res_type not match!'.format(name)
            raise IMAlreadyExist(info)
        # 實際更新
        filename = _filename(e_res)
        self.__tar.update(filename, data = data, mode = 'bytes')

    def extra_update(self, key, obj):
        '''用 obj 更新以 key 為鍵值的 extra 資料'''
        extrapath = _vpath.extra(key)
        data = pickle.dumps(obj, protocol = 3)
        self.__tar.update(name = extrapath, data = data, mode = 'bytes')
        # 清除緩存
        self.extra.cache_clear()

        # key 記錄更新
        extra_keys = self.extra_keys()
        if key not in extra_keys:
            extra_keys.add(key)
            extra_keys_data = pickle.dumps(extra_keys, protocol = 3)
            self.__tar.update(name = _vpath.extrakeys,
                    data = extra_keys_data, mode = 'bytes')
            self.extra_keys.cache_clear()

    # 新建功能

    def note_create(self, title = None,
            parent_uuid = None, order = None, new_uuid = None):
        '''在 parent_uuid 底下，新增一個新筆記頁

            title       = note 的標題。
                          None 表示自動生成
            parent_uuid = 父結點的 uuid。
                          None 表示父節點為筆記簿的根結點
            order       = 在父節點下的排序順序（0為最前）
                          None 表示排在當前的最後位置。
            new_uuid    = 新筆記頁的 uuid，不可和現存 uuid 衝突
                          None 表示自動產生
            '''
        def check_uuid_not_used(uuid):
            '''確認 uuid 沒被用過，否則拋出 IMAlreadyExist'''
            try:
                self.__e_note(uuid) # uuid 沒用過，這行會拋出 IMNotExist
                # 否則就是用過了
                info = 'uuid = "{}" are already used!'.format(uuid)
                raise IMAlreadyExist(info)
            except IMNotExist: pass # 遇到例外才是正確的

        # 檢驗並產生 uuid
        if new_uuid is not None:
            check_uuid_not_used(new_uuid)
        else:
            new_uuid = str(UM.uuid4()) # 假設產生的 uuid 不會產生碰撞
        # 尋找 parent
        e_parent = self.__e_note(parent_uuid)
        # 如果 title 不存在，自動產生 title
        if title is None:
            occupied_titles = self.__children_title(e_parent)
            title = lonote.civa.slots.slotof(
                    IM.unnamed_note_name, restrict = occupied_titles)
        # 產生 enote
        enote = _gen_enote(new_uuid, title)
        # 拼入並排序
        e_parent.append(enote)
        self.position_update(new_uuid, parent_uuid, order)
        # 用 update_content() 初始化頁面的 content
        self.content_update(new_uuid, IM.new_note_content)

        return new_uuid # 回傳

    def record_create(self, uuid, name = None,
            comment = 'No comment', content = None, ftime = None):
        '''新建一筆新的歷史記錄

            如果資料有重複，則會拋出 IMAlreadyExist 錯誤

            uuid    = 當前筆記的 uuid
            name    = 這筆記錄的名稱，不可和現有名稱重複
                      None 表示透過時間戳自動產生。
            comment = 記錄的註解
            content = 這筆記錄的資料
                      如果沒有給定，則使用當前 content
            ftime   = 記錄的凍結時間，使用 datetime 格式
                      在 data 未給定時自動忽略本值
                      如果 data 有給且 ftime 沒有指定，則使用現在時間
            '''
        def check_name_already_exist(e_history, name):
            '''檢查同名資料是否存在'''
            match = './record[@name="{}"]'.format(name)
            already_exist_record = e_history.find(match)
            if already_exist_record is not None:
                raise IMAlreadyExist('History record name: "{}" already exist!'.format(name))

        # 依不同情形自動產生 ftime 與 content
        if content is None:
            e = self.__e_note(uuid)
            timetext = e.findtext('./modified_time')
            ftime = DT.datetime.strptime(timetext, IM.time_format)
            # 產生 content
            content = self.content(uuid)
        else:
            if ftime is None:
                ftime = DT.datetime.now(local_timezone())
            elif not ftime.tzname():
                ftime = ftime.replace(tzinfo = local_timezone())
        # 自動產生 name
        if name is None:
            utctimezone = DT.timezone(DT.timedelta(hours = 0))
            utcdt = ftime.astimezone(utctimezone)
            name = utcdt.strftime('%Y%m%dT%H%M%S%f')
        # 檢查 name 是否有重複
        e_history = self.__e_history(uuid)
        check_name_already_exist(e_history, name)
        #check_ftime_already_exist(e_history, ftime)
        # 處理 history meta
        e_record = _gen_erecord(uuid = uuid,
                name = name,
                comment = comment,
                ftime = ftime)
        e_history.append(e_record)
        self.__e_history_save(uuid, e_history)
        # 將資料存入檔案
        record_file = _filename(e_record)
        self.__tar.update(record_file, data = content, mode = 'txt')
        return name # 回傳名字

    # 刪除功能

    def note_delete(self, uuid, trashcan_uuid = 'trashcan'):
        '''刪除一個 note

            trashcan_uuid = 垃圾桶的 uuid
                            如果trashcan note不存在，新建之後再處理！預設
                            新建在root note之下。
                            如果使用非uuid的字串，則會依據字串內容產生一個
                            在本筆記中固定的trashcan uuid

            直接刪除(不使用垃圾桶)的條件：
                1. 若 trashcan_uuid == root_note uuid (包括None與空字串)。
                2. 如果筆記已經在垃圾桶之下。
                3. 如果trashcan_uuid == uuid，表示直接把垃圾筒幹掉

            return ==> 希望跳轉過去的，有效的 parent uuid
            '''
        def resource_filenames(enote):
            '''回傳 enote 所有的直屬附屬資源名稱'''
            for e in enote.iterfind('.'):
                yield e.get('name')

        def e_trashcan_get(trashcan_uuid):
            '''取得trashcan，如果沒有則新建後再取得'''
            try:
                etrashcan = self.__e_note(trashcan_uuid)
            except IMNotExist:
                self.note_create(title = '♻ Trashcan',
                        parent_uuid = None,
                        order = None,
                        new_uuid = trashcan_uuid)
                #self.content_update(trashcan_uuid, 'this is trashcan.')
                etrashcan = self.__e_note(trashcan_uuid)
            return etrashcan

        def is_trashcan_mode(uuid, trashcan_uuid):
            '''檢查是否有必要進入垃圾桶模式

                return ==> True表示進入垃圾桶模式，False表示直接刪掉
                '''
            # 先檢查 1.垃圾桶不是根筆記 且 2.要刪的不是垃圾桶本身
            if not self.__is_root(trashcan_uuid)\
                and not (trashcan_uuid == uuid):
                # 再檢查： 3.原本不在垃圾筒中
                etrash = e_trashcan_get(trashcan_uuid)
                match = './/note[@uuid="{}"]'.format(uuid)
                etest = etrash.find(match)
                if etest is None:
                    return True # 不在垃圾桶中
                else:
                    return False
            else:
                return False

        def delete_recu(uuid):
            '''遞歸刪除所有子項目，而且深度優先'''
            enote = self.__e_note(uuid)
            for subnote in enote.findall('./note'):
                delete_recu(subnote.get('uuid'))
            # 遞歸步驟完成，開始刪除所有歷史記錄
            # (history meta 會隨著record delete掃蕩乾淨被自動清掉)
            e_history = self.__e_history(uuid)
            record_names = [e_record.get('name') for e_record
                    in e_history.iterfind('./record')]
            record_delete = lambda name: self.record_delete(uuid = uuid, name = name) # 這行不能用 partial 做，因為 map 不能使用keyword引數，partial 又不能重排引數順序，而我要綁定的引數(uuid)偏偏在前面，要指定的引數(name)在後面
            list(map(record_delete, record_names)) # 執行
            # 刪除所有資源
            e_resource_xml = self.__e_resource_xml(uuid)
            resource_names = [e_res.get('name') for e_res in
                    e_resource_xml.iterfind('./resource')]
            res_delete = lambda name: self.resource_delete(uuid = uuid, name = name)
            list(map(res_delete, resource_names)) # 執行
            # 刪除 content
            content_filename = _filename(enote)
            self.__tar.delete(content_filename)
            # 刪除 meta
            eparent = self.__e_parent(uuid)
            eparent.remove(enote)
            self.__bookmeta_save()

        if self.__is_root(uuid):
            info = 'You can\'t delete the root note!'
            raise IMValueError(info)

        old_parent = self.__e_parent(uuid)

        # 轉譯trashcan_uuid
        try: # 確認trashcan_uuid合法則什麼都不做
            if trashcan_uuid != None:
                UM.UUID(trashcan_uuid)
        except ValueError: # 否則生成合法的trashcan uuid
            buuid = UM.UUID(self.book_uuid())
            trashcan_uuid = str(UM.uuid5(buuid ,trashcan_uuid))
        # 測試是否要丟入垃圾桶
        if is_trashcan_mode(uuid, trashcan_uuid):
            self.position_update(uuid,
                    parent_uuid = trashcan_uuid, order = None)
            self.__bookmeta_save()
        else: # 放棄使用垃圾桶模式，採用一般模式刪除……
            delete_recu(uuid)
        return_info = old_parent.get('uuid') # 回傳old parent uuid

        # 清除緩存
        self.content.cache_clear()

        return return_info

    def record_delete(self, uuid, name):
        '''刪除一筆指定的 history record

            如果 record 被掃光，則history meta file也會被清掉
            '''
        # e_record = self.__e_record(uuid, name) # 用這種方法讀取會刪不掉
        e_history = self.__e_history(uuid)
        match = './record[@name="{}"]'.format(name)
        e_record = e_history.find(match)
        if e_record is None:
            info = 'record not exist!'
            raise IMNotExist(info)
        # 刪掉檔案
        record_file = _filename(e_record)
        self.__tar.delete(record_file)
        # 刪掉meta記錄
        e_history.remove(e_record)
        self.__e_history_save(uuid, e_history) # 檔案更新

    def resource_delete(self, uuid, name):
        '''刪除指定筆記的附屬資源

            uuid    = 指定筆記的 uuid
            name    = 資源名稱，不含路徑
            '''
        e_resxml = self.__e_resource_xml(uuid)
        match = './resource[@name="{}"]'.format(name)
        e_res = e_resxml.find(match)
        # 刪除檔案
        obj_path = _filename(e_res)
        self.__tar.delete(obj_path)
        # 修改 meta
        e_resxml.remove(e_res)
        self.__e_resource_xml_save(uuid, e_resxml)

    def extra_delete(self, key):
        '''刪除 extra'''
        extrapath = _vpath.extra(key)
        self.__tar.delete(extrapath)
        # 清除緩存
        self.extra.cache_clear()
